/*
 * PhotosPanel.java 5 Nov 2012
 *
 * Sweet Home 3D, Copyright (c) 2012 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.swing;

import java.awt.CardLayout;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import javax.swing.AbstractAction;
import javax.swing.AbstractListModel;
import javax.swing.ActionMap;
import javax.swing.BoundedRangeModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import com.eteks.sweethome3d.j3d.PhotoRenderer;
import com.eteks.sweethome3d.model.Camera;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.Selectable;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.viewcontroller.ContentManager;
import com.eteks.sweethome3d.viewcontroller.DialogView;
import com.eteks.sweethome3d.viewcontroller.Object3DFactory;
import com.eteks.sweethome3d.viewcontroller.PhotosController;
import com.eteks.sweethome3d.viewcontroller.View;

/**
 * A panel to edit photos created at home points of view. 
 * @author Emmanuel Puybaret
 */
public class PhotosPanel extends JPanel implements DialogView
{
	private enum ActionType
	{
		START_PHOTOS_CREATION, STOP_PHOTOS_CREATION, CLOSE
	}
	
	private static final String PHOTOS_DIALOG_X_VISUAL_PROPERTY = "com.eteks.sweethome3d.swing.PhotosPanel.PhotoDialogX";
	private static final String PHOTOS_DIALOG_Y_VISUAL_PROPERTY = "com.eteks.sweethome3d.swing.PhotosPanel.PhotoDialogY";
	
	private static final String TIP_CARD = "tip";
	private static final String PROGRESS_CARD = "progress";
	private static final String END_CARD = "end";
	
	private final Home home;
	private final UserPreferences preferences;
	private final Object3DFactory object3dFactory;
	private final PhotosController controller;
	private JLabel selectedCamerasLabel;
	private JList selectedCamerasList;
	private CardLayout statusLayout;
	private JPanel statusPanel;
	private JLabel tipLabel;
	private JLabel progressLabel;
	private JProgressBar progressBar;
	private JLabel endLabel;
	private ScaledImageComponent photoComponent;
	private PhotoSizeAndQualityPanel sizeAndQualityPanel;
	private JLabel fileFormatLabel;
	private JComboBox fileFormatComboBox;
	private String dialogTitle;
	private ExecutorService photosCreationExecutor;
	private JButton startStopButton;
	private JButton closeButton;
	
	private static PhotosPanel currentPhotosPanel; // Support only one photos panel opened at a time
	
	public PhotosPanel(Home home, UserPreferences preferences, PhotosController controller)
	{
		this(home, preferences, null, controller);
	}
	
	public PhotosPanel(Home home, UserPreferences preferences, Object3DFactory object3dFactory,
			PhotosController controller)
	{
		super(new GridBagLayout());
		this.home = home;
		this.preferences = preferences;
		this.object3dFactory = object3dFactory;
		this.controller = controller;
		createActions(preferences);
		createComponents(home, preferences, controller);
		setMnemonics(preferences);
		layoutComponents();
		
		preferences.addPropertyChangeListener(UserPreferences.Property.LANGUAGE, new LanguageChangeListener(this));
	}
	
	/**
	 * Creates actions for variables.
	 */
	private void createActions(UserPreferences preferences)
	{
		final ActionMap actions = getActionMap();
		actions.put(ActionType.START_PHOTOS_CREATION,
				new ResourceAction(preferences, PhotosPanel.class, ActionType.START_PHOTOS_CREATION.name(), true)
				{
					@Override
					public void actionPerformed(ActionEvent ev)
					{
						startPhotosCreation();
					}
				});
		actions.put(ActionType.STOP_PHOTOS_CREATION,
				new ResourceAction(preferences, PhotosPanel.class, ActionType.STOP_PHOTOS_CREATION.name(), true)
				{
					@Override
					public void actionPerformed(ActionEvent ev)
					{
						stopPhotosCreation();
					}
				});
		actions.put(ActionType.CLOSE, new ResourceAction(preferences, PhotosPanel.class, ActionType.CLOSE.name(), true)
		{
			@Override
			public void actionPerformed(ActionEvent ev)
			{
				close();
			}
		});
	}
	
	/**
	 * Creates and initializes components.
	 */
	private void createComponents(final Home home, final UserPreferences preferences, final PhotosController controller)
	{
		// Create selected cameras label and list bound to SELECTED_CAMERAS controller property
		this.selectedCamerasLabel = new JLabel();
		this.selectedCamerasList = new JList(new CamerasListModel());
		this.selectedCamerasList.setCellRenderer(new ListCellRenderer()
		{
			private JCheckBox cameraCheckBox = new JCheckBox();
			
			public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
					boolean cellHasFocus)
			{
				this.cameraCheckBox.setText(((Camera) value).getName());
				this.cameraCheckBox.setSelected(controller.getSelectedCameras().contains(value));
				this.cameraCheckBox.setOpaque(true);
				if (isSelected && list.hasFocus())
				{
					this.cameraCheckBox.setBackground(list.getSelectionBackground());
					this.cameraCheckBox.setForeground(list.getSelectionForeground());
				}
				else
				{
					this.cameraCheckBox.setBackground(list.getBackground());
					this.cameraCheckBox.setForeground(list.getForeground());
				}
				return this.cameraCheckBox;
			}
		});
		this.selectedCamerasList.addMouseListener(new MouseAdapter()
		{
			@Override
			public void mouseClicked(MouseEvent ev)
			{
				if (selectedCamerasList.isEnabled())
				{
					int index = selectedCamerasList.locationToIndex(ev.getPoint());
					if (index >= 0)
					{
						toggleCameraSelection((Camera) selectedCamerasList.getModel().getElementAt(index), controller);
					}
				}
			}
		});
		this.selectedCamerasList.getInputMap().put(KeyStroke.getKeyStroke("pressed SPACE"), "toggleSelection");
		this.selectedCamerasList.getActionMap().put("toggleSelection", new AbstractAction()
		{
			public void actionPerformed(ActionEvent ev)
			{
				if (selectedCamerasList.isEnabled())
				{
					Camera camera = (Camera) selectedCamerasList.getSelectedValue();
					if (camera != null)
					{
						toggleCameraSelection(camera, controller);
					}
				}
			}
		});
		this.selectedCamerasList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		this.selectedCamerasList.setSelectedIndex(0);
		controller.addPropertyChangeListener(PhotosController.Property.SELECTED_CAMERAS, new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				selectedCamerasList.repaint();
				getActionMap().get(ActionType.START_PHOTOS_CREATION).setEnabled(!((List) ev.getNewValue()).isEmpty());
				if (startStopButton.getAction() == getActionMap().get(ActionType.START_PHOTOS_CREATION))
				{
					statusLayout.show(statusPanel, TIP_CARD);
				}
			}
		});
		controller.addPropertyChangeListener(PhotosController.Property.CAMERAS, new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				selectedCamerasList.repaint();
			}
		});
		
		// Create tip / progress / end components
		this.tipLabel = new JLabel();
		Font toolTipFont = UIManager.getFont("ToolTip.font");
		this.tipLabel.setFont(toolTipFont);
		
		this.progressLabel = new JLabel();
		this.progressLabel.setFont(toolTipFont);
		this.progressLabel.setHorizontalAlignment(JLabel.CENTER);
		
		this.progressBar = new JProgressBar();
		this.progressBar.setIndeterminate(true);
		this.progressBar.getModel().addChangeListener(new ChangeListener()
		{
			public void stateChanged(ChangeEvent ev)
			{
				int progressValue = progressBar.getValue();
				progressBar.setIndeterminate(progressValue < 0);
				if (progressValue >= 0)
				{
					progressLabel.setText(preferences.getLocalizedString(PhotosPanel.class, "progressLabel.format",
							progressValue + 1, progressBar.getMaximum()));
				}
			}
		});
		
		this.endLabel = new JLabel();
		this.endLabel.setFont(toolTipFont);
		this.endLabel.setHorizontalAlignment(JLabel.CENTER);
		
		this.photoComponent = new ScaledImageComponent();
		this.photoComponent.setPreferredSize(new Dimension(toolTipFont.getSize() * 5, toolTipFont.getSize() * 5));
		
		// Create size and quality panel
		this.sizeAndQualityPanel = new PhotoSizeAndQualityPanel(home, preferences, controller);
		
		// Create file format label and combo box bound to FILE_FORMAT / FILE_COMPRESSION_QUALITY controller properties
		this.fileFormatLabel = new JLabel();
		this.fileFormatComboBox = new JComboBox(new Object[] { "PNG", "JPEG 0.3", "JPEG 0.5", "JPEG 0.7", "JPEG 0.9" });
		this.fileFormatComboBox.setRenderer(new DefaultListCellRenderer()
		{
			@Override
			public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
					boolean cellHasFocus)
			{
				String string = (String) value;
				String displayedValue = "";
				if ("PNG".equals(string))
				{
					displayedValue = preferences.getLocalizedString(PhotosPanel.class, "fileFormatComboBox.png.text");
				}
				else if (((String) string).startsWith("JPEG"))
				{
					float compressionQuality = Float.parseFloat(string.substring(string.lastIndexOf(' ') + 1));
					displayedValue = preferences.getLocalizedString(PhotosPanel.class, "fileFormatComboBox.jpeg.text",
							Math.round(compressionQuality * 100));
				}
				return super.getListCellRendererComponent(list, displayedValue, index, isSelected, cellHasFocus);
			}
		});
		ItemListener fileFormatItemListener = new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				String value = (String) fileFormatComboBox.getSelectedItem();
				if (value.startsWith("JPEG"))
				{
					controller.setFileFormat("JPEG");
					controller.setFileCompressionQuality(new Float(value.substring(value.lastIndexOf(' ') + 1)));
				}
				else
				{
					controller.setFileFormat("PNG");
					controller.setFileCompressionQuality(null);
				}
			}
		};
		this.fileFormatComboBox.addItemListener(fileFormatItemListener);
		PropertyChangeListener fileFormatChangeListener = new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				fileFormatComboBox
						.setSelectedItem(controller.getFileFormat() + (controller.getFileCompressionQuality() != null
								? " " + controller.getFileCompressionQuality() : ""));
			}
		};
		controller.addPropertyChangeListener(PhotosController.Property.FILE_FORMAT, fileFormatChangeListener);
		controller.addPropertyChangeListener(PhotosController.Property.FILE_COMPRESSION_QUALITY,
				fileFormatChangeListener);
		if (controller.getFileFormat() != null)
		{
			fileFormatChangeListener.propertyChange(null);
		}
		else
		{
			fileFormatItemListener.itemStateChanged(null);
		}
		
		final JComponent view3D = (JComponent) controller.get3DView();
		controller.set3DViewAspectRatio((float) view3D.getWidth() / view3D.getHeight());
		
		final ActionMap actionMap = getActionMap();
		this.startStopButton = new JButton(actionMap.get(ActionType.START_PHOTOS_CREATION));
		this.closeButton = new JButton(actionMap.get(ActionType.CLOSE));
		
		setComponentTexts(preferences);
	}
	
	/**
	 * Toggles the selected status of the given <code>camera</code>.
	 */
	private void toggleCameraSelection(Camera camera, final PhotosController controller)
	{
		List<Camera> selectedCameras = new ArrayList<Camera>(controller.getSelectedCameras());
		if (selectedCameras.contains(camera))
		{
			selectedCameras.remove(camera);
		}
		else
		{
			selectedCameras.add(camera);
		}
		controller.setSelectedCameras(selectedCameras);
	}
	
	/**
	 * Sets the texts of the components.
	 */
	private void setComponentTexts(UserPreferences preferences)
	{
		this.tipLabel.setText(preferences.getLocalizedString(PhotosPanel.class, "tipLabel.text"));
		this.endLabel.setText(preferences.getLocalizedString(PhotosPanel.class, "endLabel.text"));
		this.selectedCamerasLabel
				.setText(SwingTools.getLocalizedLabelText(preferences, PhotosPanel.class, "selectedCamerasLabel.text"));
		this.fileFormatLabel
				.setText(SwingTools.getLocalizedLabelText(preferences, PhotosPanel.class, "fileFormatLabel.text"));
		this.dialogTitle = preferences.getLocalizedString(PhotosPanel.class, "createPhotos.title");
		Window window = SwingUtilities.getWindowAncestor(this);
		if (window != null)
		{
			((JDialog) window).setTitle(this.dialogTitle);
		}
		// Buttons text changes automatically through their action
	}
	
	/**
	 * Sets components mnemonics and label / component associations.
	 */
	private void setMnemonics(UserPreferences preferences)
	{
		if (!OperatingSystem.isMacOSX())
		{
			this.selectedCamerasLabel.setDisplayedMnemonic(KeyStroke
					.getKeyStroke(preferences.getLocalizedString(PhotosPanel.class, "selectedCamerasLabel.mnemonic"))
					.getKeyCode());
			this.selectedCamerasLabel.setLabelFor(this.selectedCamerasList);
			this.fileFormatLabel.setDisplayedMnemonic(KeyStroke
					.getKeyStroke(preferences.getLocalizedString(PhotosPanel.class, "fileFormatLabel.mnemonic"))
					.getKeyCode());
			this.fileFormatLabel.setLabelFor(this.fileFormatComboBox);
		}
	}
	
	/**
	 * Preferences property listener bound to this panel with a weak reference to avoid
	 * strong link between user preferences and this panel.  
	 */
	public static class LanguageChangeListener implements PropertyChangeListener
	{
		private final WeakReference<PhotosPanel> photoPanel;
		
		public LanguageChangeListener(PhotosPanel photoPanel)
		{
			this.photoPanel = new WeakReference<PhotosPanel>(photoPanel);
		}
		
		public void propertyChange(PropertyChangeEvent ev)
		{
			// If photo panel was garbage collected, remove this listener from preferences
			PhotosPanel photoPanel = this.photoPanel.get();
			UserPreferences preferences = (UserPreferences) ev.getSource();
			if (photoPanel == null)
			{
				preferences.removePropertyChangeListener(UserPreferences.Property.LANGUAGE, this);
			}
			else
			{
				photoPanel.setComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault()));
				photoPanel.setComponentTexts(preferences);
				photoPanel.setMnemonics(preferences);
			}
		}
	}
	
	/**
	 * Layouts panel components in panel with their labels. 
	 */
	private void layoutComponents()
	{
		int labelAlignment = OperatingSystem.isMacOSX() ? JLabel.TRAILING : JLabel.LEADING;
		// Add tip and progress bar to a card panel 
		this.statusLayout = new CardLayout();
		this.statusPanel = new JPanel(this.statusLayout);
		this.statusPanel.add(this.tipLabel, TIP_CARD);
		this.tipLabel.setMinimumSize(this.tipLabel.getPreferredSize());
		JPanel progressPanel = new JPanel(new GridBagLayout());
		progressPanel.add(this.photoComponent, new GridBagConstraints(0, 0, 1, 2, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
		progressPanel.add(this.progressBar, new GridBagConstraints(1, 0, 1, 1, 1, 1, GridBagConstraints.SOUTH,
				GridBagConstraints.HORIZONTAL, new Insets(0, 5, 5, 0), 0, 0));
		progressPanel.add(this.progressLabel, new GridBagConstraints(1, 1, 1, 1, 0, 1, GridBagConstraints.NORTH,
				GridBagConstraints.NONE, new Insets(0, 5, 0, 0), 0, 0));
		this.statusPanel.add(progressPanel, PROGRESS_CARD);
		this.statusPanel.add(this.endLabel, END_CARD);
		this.endLabel.setMinimumSize(this.endLabel.getPreferredSize());
		// First row
		add(this.selectedCamerasLabel, new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
		// Second row
		add(SwingTools.createScrollPane(this.selectedCamerasList), new GridBagConstraints(0, 1, 1, 1, 1, 1,
				GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 5, 0), 0, 0));
		// Third row
		add(this.statusPanel, new GridBagConstraints(0, 2, 1, 1, 1, 0, GridBagConstraints.CENTER,
				GridBagConstraints.NONE, new Insets(0, 0, 10, 0), 0, 0));
		// Fourth row
		add(this.sizeAndQualityPanel, new GridBagConstraints(0, 3, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
		// Last row
		JPanel fileFormatPanel = new JPanel();
		fileFormatPanel.add(this.fileFormatLabel);
		fileFormatPanel.add(this.fileFormatComboBox);
		this.fileFormatLabel.setHorizontalAlignment(labelAlignment);
		add(fileFormatPanel, new GridBagConstraints(0, 7, 4, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.NONE, new Insets(10, 0, 0, 0), 0, 0));
	}
	
	/**
	 * Displays this panel in a non modal dialog.
	 */
	public void displayView(View parentView)
	{
		if (currentPhotosPanel == this)
		{
			SwingUtilities.getWindowAncestor(PhotosPanel.this).toFront();
		}
		else
		{
			if (currentPhotosPanel != null)
			{
				currentPhotosPanel.close();
			}
			final JOptionPane optionPane = new JOptionPane(this, JOptionPane.PLAIN_MESSAGE,
					JOptionPane.OK_CANCEL_OPTION, null, new Object[] { this.startStopButton, this.closeButton },
					this.startStopButton);
			if (parentView != null)
			{
				optionPane.setComponentOrientation(((JComponent) parentView).getComponentOrientation());
			}
			final JDialog dialog = optionPane.createDialog(SwingUtilities.getRootPane((Component) parentView),
					this.dialogTitle);
			dialog.setModal(false);
			
			Component homeRoot = SwingUtilities.getRoot((Component) parentView);
			Point dialogLocation = null;
			if (homeRoot != null)
			{
				// Restore location if it exists
				Number x = this.home.getNumericProperty(PHOTOS_DIALOG_X_VISUAL_PROPERTY);
				Number y = this.home.getNumericProperty(PHOTOS_DIALOG_Y_VISUAL_PROPERTY);
				
				int windowRightBorder = homeRoot.getX() + homeRoot.getWidth();
				Dimension screenSize = getToolkit().getScreenSize();
				Insets screenInsets = getToolkit().getScreenInsets(getGraphicsConfiguration());
				int screenRightBorder = screenSize.width - screenInsets.right;
				// Check dialog isn't too high
				int screenHeight = screenSize.height - screenInsets.top - screenInsets.bottom;
				if (OperatingSystem.isLinux() && screenHeight == screenSize.height)
				{
					// Let's consider that under Linux at least an horizontal bar exists 
					screenHeight -= 30;
				}
				int screenBottomBorder = screenSize.height - screenInsets.bottom;
				int dialogWidth = dialog.getWidth();
				if (dialog.getHeight() > screenHeight)
				{
					dialog.setSize(dialogWidth, screenHeight);
				}
				int dialogHeight = dialog.getHeight();
				if (x != null && y != null && x.intValue() + dialogWidth <= screenRightBorder
						&& y.intValue() + dialogHeight <= screenBottomBorder)
				{
					dialogLocation = new Point(x.intValue(), y.intValue());
				}
				else if (screenRightBorder - windowRightBorder > dialogWidth / 2 || dialogHeight == screenHeight)
				{
					// If there some space left at the right of the window,
					// move the dialog to the right of window
					dialogLocation = new Point(Math.min(windowRightBorder + 5, screenRightBorder - dialogWidth),
							Math.max(Math.min(homeRoot.getY(), screenSize.height - dialogHeight - screenInsets.bottom),
									screenInsets.top));
				}
			}
			if (dialogLocation != null
					&& SwingTools.isRectangleVisibleAtScreen(new Rectangle(dialogLocation, dialog.getSize())))
			{
				dialog.setLocationByPlatform(false);
				dialog.setLocation(dialogLocation);
			}
			else
			{
				dialog.setLocationByPlatform(true);
			}
			
			dialog.addWindowListener(new WindowAdapter()
			{
				public void windowClosed(WindowEvent ev)
				{
					stopPhotosCreation();
					currentPhotosPanel = null;
				}
			});
			dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
			dialog.addComponentListener(new ComponentAdapter()
			{
				@Override
				public void componentHidden(ComponentEvent ev)
				{
					if (optionPane.getValue() != null && optionPane.getValue() != JOptionPane.UNINITIALIZED_VALUE)
					{
						close();
					}
				}
				
				@Override
				public void componentMoved(ComponentEvent ev)
				{
					controller.setHomeProperty(PHOTOS_DIALOG_X_VISUAL_PROPERTY, String.valueOf(dialog.getX()));
					controller.setHomeProperty(PHOTOS_DIALOG_Y_VISUAL_PROPERTY, String.valueOf(dialog.getY()));
				}
			});
			dialog.setVisible(true);
			currentPhotosPanel = this;
		}
	}
	
	/**
	 * Creates the photo images in a folder depending on the quality requested by the user.
	 */
	private void startPhotosCreation()
	{
		final String directory = this.controller.getContentManager().showSaveDialog(this,
				this.preferences.getLocalizedString(PhotosPanel.class, "selectPhotosFolderDialog.title"),
				ContentManager.ContentType.PHOTOS_DIRECTORY, this.home.getName());
		if (directory != null)
		{
			// Build file names list
			final Map<Camera, File> cameraFiles = new LinkedHashMap<Camera, File>();
			List<Camera> selectedCameras = this.controller.getSelectedCameras();
			ContentManager contentManager = this.controller.getContentManager();
			boolean overwriteConfirmed = false;
			for (Camera camera : this.controller.getCameras())
			{
				if (selectedCameras.contains(camera))
				{
					String fileName = "";
					if (this.home.getName() != null)
					{
						fileName += contentManager.getPresentationName(this.home.getName(),
								ContentManager.ContentType.SWEET_HOME_3D);
						if (contentManager instanceof FileContentManager)
						{
							fileName = fileName.substring(0,
									fileName.length() - ((FileContentManager) contentManager)
											.getDefaultFileExtension(ContentManager.ContentType.SWEET_HOME_3D)
											.length());
						}
						fileName += " - ";
					}
					fileName += camera.getName();
					fileName = fileName.replaceAll("/|\\\\|:|;", "-").replace(File.pathSeparatorChar, '-')
							.replace(File.separatorChar, '-');
					if (contentManager instanceof FileContentManager)
					{
						fileName += ((FileContentManager) contentManager).getDefaultFileExtension(
								ContentManager.ContentType.valueOf(controller.getFileFormat()));
					}
					File cameraFile = new File(directory, fileName);
					if (!overwriteConfirmed && cameraFile.exists())
					{
						if (JOptionPane.showConfirmDialog(this,
								this.preferences.getLocalizedString(PhotosPanel.class, "confirmOverwrite.message",
										directory),
								this.preferences.getLocalizedString(PhotosPanel.class, "confirmOverwrite.title"),
								JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION)
						{
							return;
						}
						else
						{
							overwriteConfirmed = true;
						}
					}
					cameraFiles.put(camera, cameraFile);
				}
			}
			
			this.photoComponent.setImage(null);
			this.selectedCamerasList.setEnabled(false);
			this.sizeAndQualityPanel.setEnabled(false);
			this.fileFormatComboBox.setEnabled(false);
			getRootPane().setDefaultButton(this.startStopButton);
			this.startStopButton.setAction(getActionMap().get(ActionType.STOP_PHOTOS_CREATION));
			this.statusLayout.show(this.statusPanel, PROGRESS_CARD);
			this.progressBar.setMinimum(-1);
			this.progressBar.setValue(-1);
			this.progressLabel.setText("");
			this.photoComponent.setImage(null);
			
			// Compute photos in an other executor thread
			// Use a clone of home because the user can modify home during photos computation
			final Home home = this.home.clone();
			List<Selectable> emptySelection = Collections.emptyList();
			home.setSelectedItems(emptySelection);
			this.photosCreationExecutor = Executors.newSingleThreadExecutor();
			this.photosCreationExecutor.execute(new Runnable()
			{
				public void run()
				{
					computePhotos(home, cameraFiles);
				}
			});
		}
	}
	
	/**
	 * Computes the photo of the given home.
	 * Caution : this method must be thread safe because it's called from an executor. 
	 */
	private void computePhotos(Home home, final Map<Camera, File> cameraFiles)
	{
		BufferedImage image = null;
		boolean success = false;
		try
		{
			int photoIndex = 0;
			for (Map.Entry<Camera, File> cameraEntry : cameraFiles.entrySet())
			{
				int quality = this.controller.getQuality();
				int imageWidth = this.controller.getWidth();
				int imageHeight = this.controller.getHeight();
				Camera camera = cameraEntry.getKey();
				home.setCamera(camera);
				if (quality >= 2)
				{
					// Use photo renderer
					PhotoRenderer photoRenderer = new PhotoRenderer(home, this.object3dFactory,
							quality == 2 ? PhotoRenderer.Quality.LOW : PhotoRenderer.Quality.HIGH);
					int bestImageHeight;
					// Update ratio if lens is fisheye or spherical
					if (camera.getLens() == Camera.Lens.FISHEYE)
					{
						bestImageHeight = imageWidth;
					}
					else if (camera.getLens() == Camera.Lens.SPHERICAL)
					{
						bestImageHeight = imageWidth / 2;
					}
					else
					{
						bestImageHeight = imageHeight;
					}
					if (this.photosCreationExecutor != null)
					{
						image = new BufferedImage(imageWidth, bestImageHeight, BufferedImage.TYPE_INT_RGB);
						this.photoComponent.setImage(image);
						updateProgressBar(photoIndex++, cameraFiles.size());
						photoRenderer.render(image, camera, this.photoComponent);
					}
				}
				else
				{
					// Compute 3D view offscreen image
					HomeComponent3D homeComponent3D = new HomeComponent3D(home, this.preferences, this.object3dFactory,
							quality == 1, null);
					updateProgressBar(photoIndex++, cameraFiles.size());
					image = homeComponent3D.getOffScreenImage(imageWidth, imageHeight);
					this.photoComponent.setImage(image);
				}
				
				try
				{
					if (this.photosCreationExecutor != null)
					{
						savePhoto(image, cameraEntry.getValue());
					}
				}
				catch (final IOException ex)
				{
					showPhotoSaveError(ex);
					return;
				}
				
				if (this.photosCreationExecutor == null)
				{
					return;
				}
			}
			success = true;
		}
		catch (OutOfMemoryError ex)
		{
			showPhotosComputingError(ex);
		}
		catch (IllegalStateException ex)
		{
			showPhotosComputingError(ex);
		}
		catch (IOException ex)
		{
			showPhotosComputingError(ex);
		}
		finally
		{
			final boolean succeeded = success;
			EventQueue.invokeLater(new Runnable()
			{
				public void run()
				{
					startStopButton.setAction(getActionMap().get(ActionType.START_PHOTOS_CREATION));
					selectedCamerasList.setEnabled(true);
					sizeAndQualityPanel.setEnabled(true);
					sizeAndQualityPanel.setProportionsChoiceEnabled(true);
					fileFormatComboBox.setEnabled(true);
					if (succeeded)
					{
						statusLayout.show(statusPanel, END_CARD);
					}
					else
					{
						statusLayout.show(statusPanel, TIP_CARD);
					}
					photosCreationExecutor = null;
				}
			});
		}
	}
	
	private void updateProgressBar(final int photoIndex, final int photoCount)
	{
		final BoundedRangeModel progressModel = this.progressBar.getModel();
		EventQueue.invokeLater(new Runnable()
		{
			public void run()
			{
				progressModel.setMinimum(0);
				progressModel.setMaximum(photoCount);
				progressModel.setValue(photoIndex);
			}
		});
	}
	
	/**
	 * Displays an error message box for save errors.
	 */
	private void showPhotoSaveError(final Throwable ex)
	{
		try
		{
			EventQueue.invokeAndWait(new Runnable()
			{
				public void run()
				{
					String messageFormat = preferences.getLocalizedString(PhotosPanel.class, "savePhotosError.message");
					JOptionPane.showMessageDialog(SwingUtilities.getRootPane(PhotosPanel.this),
							String.format(messageFormat, ex.getMessage()),
							preferences.getLocalizedString(PhotosPanel.class, "savePhotosError.title"),
							JOptionPane.ERROR_MESSAGE);
				}
			});
		}
		catch (InterruptedException ex1)
		{
			ex1.printStackTrace();
		}
		catch (InvocationTargetException ex1)
		{
			throw new RuntimeException(ex1);
		}
	}
	
	/**
	 * Displays an error message box.
	 */
	public void showPhotosComputingError(Throwable exception)
	{
		EventQueue.invokeLater(new Runnable()
		{
			public void run()
			{
				String title = preferences.getLocalizedString(PhotosPanel.class, "error.title");
				String message = preferences.getLocalizedString(PhotosPanel.class, "error.message");
				JOptionPane.showMessageDialog(PhotosPanel.this, message, title, JOptionPane.ERROR_MESSAGE);
			}
		});
	}
	
	/**
	 * Stops photos creation.
	 */
	private void stopPhotosCreation()
	{
		if (this.photosCreationExecutor != null)
		{
			// Will interrupt executor thread      
			this.photosCreationExecutor.shutdownNow();
			this.photosCreationExecutor = null;
			this.startStopButton.setAction(getActionMap().get(ActionType.START_PHOTOS_CREATION));
		}
	}
	
	/**
	 * Saves the given <code>image</code>.
	 */
	private void savePhoto(BufferedImage image, File file) throws IOException
	{
		String fileFormat = this.controller.getFileFormat();
		Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName(fileFormat);
		ImageWriter writer = (ImageWriter) iter.next();
		ImageWriteParam writeParam = writer.getDefaultWriteParam();
		if ("JPEG".equals(fileFormat))
		{
			writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
			writeParam.setCompressionQuality(this.controller.getFileCompressionQuality());
		}
		FileImageOutputStream output = new FileImageOutputStream(file);
		writer.setOutput(output);
		writer.write(null, new IIOImage(image, null, null), writeParam);
		writer.dispose();
		output.close();
	}
	
	/**
	 * Manages closing of this pane.
	 */
	private void close()
	{
		Window window = SwingUtilities.getWindowAncestor(this);
		if (window.isDisplayable())
		{
			window.dispose();
		}
	}
	
	/**
	 * List model for cameras.
	 */
	private final class CamerasListModel extends AbstractListModel
	{
		private List<Camera> cameras;
		
		public CamerasListModel()
		{
			this.cameras = controller.getCameras();
			controller.addPropertyChangeListener(PhotosController.Property.CAMERAS, new PropertyChangeListener()
			{
				public void propertyChange(PropertyChangeEvent ev)
				{
					cameras = controller.getCameras();
					fireContentsChanged(this, 0, cameras.size());
				}
			});
		}
		
		public int getSize()
		{
			return this.cameras.size();
		}
		
		public Object getElementAt(int index)
		{
			return this.cameras.get(index);
		}
	}
}
